use std::{borrow::Cow, collections::BTreeSet};

use ahash::{HashMap, HashMapExt};
use anyhow::Context;
use camino::Utf8Path;
use guppy::{
    PackageId,
    graph::{PackageGraph, PackageMetadata, feature::StandardFeatures},
};
use itertools::Itertools;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
use tracing::instrument;
use tracing_log_error::log_error;

use crate::{
    DiagnosticSink,
    rustdoc::{
        annotations::AnnotatedItems,
        queries::{CrateData, CrateItemIndex, LazyCrateItemIndex},
    },
};

use super::{checksum::checksum_crate, rustdoc_options};

/// A cache for storing and retrieving pre-computed JSON documentation generated by `rustdoc`.
///
/// The cache is shared across all Pavex projects of the current user.
/// It is stored on disk, in the user home directory, using a SQLite database.
#[derive(Debug, Clone)]
pub(crate) struct RustdocGlobalFsCache {
    cargo_fingerprint: String,
    third_party_cache: ThirdPartyCrateCache,
    toolchain_cache: ToolchainCache,
    connection_pool: r2d2::Pool<SqliteConnectionManager>,
}

pub(crate) enum RustdocCacheKey<'a> {
    ThirdPartyCrate(PackageMetadata<'a>),
    ToolchainCrate(&'a str),
}

impl std::fmt::Debug for RustdocCacheKey<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RustdocCacheKey::ThirdPartyCrate(metadata) => f
                .debug_struct("ThirdPartyCrate")
                .field("id", &metadata.id())
                .field("name", &metadata.name())
                .field("version", &metadata.version())
                .finish(),
            RustdocCacheKey::ToolchainCrate(name) => f
                .debug_struct("ToolchainCrate")
                .field("name", name)
                .finish(),
        }
    }
}

/// An entry retrieved from the on-disk cache.
pub(crate) enum RustdocCacheEntry {
    /// Only the "raw" output returned by `rustdoc` was stored in the cache.
    ///
    /// This happens when the indexing phase emitted one or more diagnostics,
    /// thus forcing to go through that step (and report those errors)
    /// every single time we attempt a compilation.
    Raw(CrateData),
    /// The cache holds both the raw `rustdoc` output and our secondary indexes.
    /// It's ready to be used as is!
    Processed(crate::rustdoc::Crate),
}

impl RustdocCacheEntry {
    pub fn process(self, package_id: PackageId, sink: &DiagnosticSink) -> crate::rustdoc::Crate {
        match self {
            RustdocCacheEntry::Raw(crate_data) => {
                crate::rustdoc::Crate::index(crate_data, package_id, sink)
            }
            RustdocCacheEntry::Processed(c) => c,
        }
    }
}

static BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();

impl<'a> RustdocCacheKey<'a> {
    pub fn new(package_id: &'a PackageId, package_graph: &'a PackageGraph) -> RustdocCacheKey<'a> {
        if crate::rustdoc::TOOLCHAIN_CRATES.contains(&package_id.repr()) {
            RustdocCacheKey::ToolchainCrate(package_id.repr())
        } else {
            RustdocCacheKey::ThirdPartyCrate(package_graph.metadata(package_id).unwrap())
        }
    }
}

impl RustdocGlobalFsCache {
    /// Initialize a new instance of the cache.
    #[tracing::instrument(name = "Initialize on-disk rustdoc cache", skip_all)]
    pub(crate) fn new(
        toolchain_name: &str,
        cache_workspace_package_docs: bool,
    ) -> Result<Self, anyhow::Error> {
        let cargo_fingerprint = cargo_fingerprint(toolchain_name)?;
        let pool = Self::setup_database()?;

        let connection = pool.get()?;
        let third_party_cache =
            ThirdPartyCrateCache::new(&connection, cache_workspace_package_docs)?;
        let toolchain_cache = ToolchainCache::new(&connection)?;
        Ok(Self {
            cargo_fingerprint,
            connection_pool: pool,
            third_party_cache,
            toolchain_cache,
        })
    }

    /// Retrieve the cached documentation for a given package, if available.
    pub(crate) fn get(
        &self,
        cache_key: &RustdocCacheKey,
        package_graph: &PackageGraph,
    ) -> Result<Option<RustdocCacheEntry>, anyhow::Error> {
        let connection = self.connection_pool.get()?;
        match cache_key {
            RustdocCacheKey::ThirdPartyCrate(metadata) => self.third_party_cache.get(
                metadata,
                &self.cargo_fingerprint,
                &connection,
                package_graph,
            ),
            RustdocCacheKey::ToolchainCrate(name) => {
                self.toolchain_cache
                    .get(name, &self.cargo_fingerprint, &connection)
            }
        }
    }

    /// Store the JSON documentation generated by `rustdoc` in the cache.
    pub(crate) fn insert(
        &self,
        cache_key: &RustdocCacheKey,
        krate: &crate::rustdoc::Crate,
        cache_indexes: bool,
        package_graph: &PackageGraph,
    ) -> Result<(), anyhow::Error> {
        let connection = self.connection_pool.get()?;
        match cache_key {
            RustdocCacheKey::ThirdPartyCrate(metadata) => self.third_party_cache.insert(
                metadata,
                krate,
                &self.cargo_fingerprint,
                &connection,
                cache_indexes,
                package_graph,
            ),
            RustdocCacheKey::ToolchainCrate(name) => {
                self.toolchain_cache
                    .insert(name, krate, &self.cargo_fingerprint, &connection)
            }
        }
    }

    #[tracing::instrument(skip_all, level = "trace")]
    /// Persist the list of package IDs that were accessed during the processing of the
    /// application blueprint for this project.
    pub(crate) fn persist_access_log(
        &self,
        package_ids: &BTreeSet<PackageId>,
        project_fingerprint: &str,
    ) -> Result<(), anyhow::Error> {
        let connection = self.connection_pool.get()?;

        let mut stmt = connection.prepare_cached(
            "INSERT INTO project2package_id_access_log (
                project_fingerprint,
                package_ids
            ) VALUES (?, ?)
            ON CONFLICT(project_fingerprint) DO UPDATE SET package_ids=excluded.package_ids;
            ",
        )?;
        stmt.execute(params![
            project_fingerprint,
            bincode::encode_to_vec(
                package_ids.iter().map(|s| s.repr()).collect_vec(),
                BINCODE_CONFIG
            )?
        ])?;

        Ok(())
    }

    #[tracing::instrument(skip_all, level = "trace")]
    /// Retrieve the list of package IDs that were accessed during the last time we processed the
    /// application blueprint for this project.
    ///
    /// Returns an empty set if no access log is found for the given project fingerprint.
    pub(crate) fn get_access_log(
        &self,
        project_fingerprint: &str,
    ) -> Result<BTreeSet<PackageId>, anyhow::Error> {
        let connection = self.connection_pool.get()?;

        let mut stmt = connection.prepare_cached(
            "SELECT package_ids FROM project2package_id_access_log WHERE project_fingerprint = ?",
        )?;
        let mut rows = stmt.query(params![project_fingerprint])?;
        let Some(row) = rows.next()? else {
            return Ok(BTreeSet::new());
        };

        let package_ids: Vec<&str> =
            bincode::borrow_decode_from_slice(row.get_ref_unwrap(0).as_bytes()?, BINCODE_CONFIG)?.0;
        Ok(package_ids.into_iter().map(PackageId::new).collect())
    }

    /// Initialize the database, creating the file and the relevant tables if they don't exist yet.
    fn setup_database() -> Result<r2d2::Pool<SqliteConnectionManager>, anyhow::Error> {
        let pavex_fingerprint =
            concat!(env!("CARGO_PKG_VERSION"), '-', env!("VERGEN_GIT_DESCRIBE"));
        let cache_dir = xdg_home::home_dir()
            .ok_or_else(|| anyhow::anyhow!("Failed to get the user's home directory"))?
            .join(".pavex/rustdoc/cache");
        fs_err::create_dir_all(&cache_dir).with_context(|| {
            format!(
                "Failed to create the cache directory at {}",
                cache_dir.to_string_lossy()
            )
        })?;

        // For the sake of simplicity, we use a different SQLite database for each version of Pavex.
        // This ensures that we don't have to worry about schema migrations.
        // The cost we pay: the user will have to re-generate the documentation for all their crates
        // when they upgrade Pavex.
        // We can improve this in the future, if needed.
        let cache_path = cache_dir.join(format!("{pavex_fingerprint}.db"));

        let manager = SqliteConnectionManager::file(cache_dir.join(cache_path));
        let pool = r2d2::Pool::builder()
            .max_size(num_cpus::get() as u32)
            .build(manager)
            .context("Failed to open/create a SQLite database to store the contents of pavex's rustdoc cache")?;

        let connection = pool.get()?;
        connection.execute(
            "CREATE TABLE IF NOT EXISTS project2package_id_access_log (
                project_fingerprint TEXT NOT NULL,
                package_ids BLOB NOT NULL,
                PRIMARY KEY (project_fingerprint)
            )",
            [],
        )?;

        Ok(pool)
    }
}

#[derive(Debug, Clone)]
#[non_exhaustive]
struct ToolchainCache {}

impl ToolchainCache {
    fn new(connection: &rusqlite::Connection) -> Result<Self, anyhow::Error> {
        Self::setup_table(connection)?;
        Ok(Self {})
    }

    /// Retrieve the cached documentation for a given toolchain crate, if available.
    #[instrument(name = "Retrieve cached toolchain docs from disk",
        skip_all,
        level=tracing::Level::DEBUG,
        fields(crate.name = %name)
    )]
    fn get(
        &self,
        name: &str,
        cargo_fingerprint: &str,
        connection: &rusqlite::Connection,
    ) -> Result<Option<RustdocCacheEntry>, anyhow::Error> {
        // Retrieve from rustdoc's output from cache, if available.
        let mut stmt = connection.prepare_cached(
            "SELECT
                root_item_id,
                external_crates,
                paths,
                format_version,
                items,
                item_id2delimiters,
                import_index,
                import_path2id,
                re_exports
            FROM rustdoc_toolchain_crates_cache
            WHERE name = ? AND cargo_fingerprint = ?",
        )?;

        let span = tracing::trace_span!("Execute query");
        let guard = span.enter();
        let mut rows = stmt.query(params![name, cargo_fingerprint])?;
        let Some(row) = rows.next()? else {
            return Ok(None);
        };
        drop(guard);

        let root_item_id = row.get_ref_unwrap(0).as_i64()?.try_into()?;
        let external_crates = row.get_ref_unwrap(1).as_bytes()?;
        let paths = row.get_ref_unwrap(2).as_bytes()?;
        let format_version = row.get_ref_unwrap(3).as_i64()?;

        let span = tracing::trace_span!("Copy items bytes buffer");
        let guard = span.enter();
        let items: Vec<u8> = row.get_unwrap(4);
        drop(guard);

        let item_id2delimiters = row.get_ref_unwrap(5).as_bytes()?;
        let import_index = row.get_ref_unwrap(6).as_bytes()?;
        let import_path2id = row.get_ref_unwrap(7).as_bytes()?;
        let re_exports = row.get_ref_unwrap(8).as_bytes()?;

        let krate = CachedData {
            root_item_id,
            external_crates: Cow::Borrowed(external_crates),
            paths: Cow::Borrowed(paths),
            format_version,
            items: Cow::Owned(items),
            item_id2delimiters: Cow::Borrowed(item_id2delimiters),
            secondary_indexes: Some(SecondaryIndexes {
                import_index: Cow::Borrowed(import_index),
                // Standard library crates don't have Pavex annotations.
                annotated_items: None,
                import_path2id: Cow::Borrowed(import_path2id),
                re_exports: Cow::Borrowed(re_exports),
            }),
        }
        .hydrate(PackageId::new(name))?;

        Ok(Some(krate))
    }

    /// Store the JSON documentation for a toolchain crate in the cache.
    #[instrument(name = "Cache rustdoc output on disk", skip_all, level=tracing::Level::DEBUG, fields(crate.name = name))]
    fn insert(
        &self,
        name: &str,
        krate: &crate::rustdoc::Crate,
        cargo_fingerprint: &str,
        connection: &rusqlite::Connection,
    ) -> Result<(), anyhow::Error> {
        let cached_data = CachedData::new(krate).context("Failed to serialize docs")?;
        let mut stmt = connection.prepare_cached(
            "INSERT INTO rustdoc_toolchain_crates_cache (
                name,
                cargo_fingerprint,
                root_item_id,
                external_crates,
                paths,
                format_version,
                items,
                item_id2delimiters,
                import_index,
                import_path2id,
                re_exports
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        )?;
        stmt.execute(params![
            name,
            cargo_fingerprint,
            cached_data.root_item_id,
            cached_data.external_crates,
            cached_data.paths,
            cached_data.format_version,
            cached_data.items,
            cached_data.item_id2delimiters,
            cached_data
                .secondary_indexes
                .as_ref()
                .expect("Indexing never fails for toolchain crates")
                .import_index,
            cached_data
                .secondary_indexes
                .as_ref()
                .expect("Indexing never fails for toolchain crates")
                .import_path2id,
            cached_data
                .secondary_indexes
                .as_ref()
                .expect("Indexing never fails for toolchain crates")
                .re_exports
        ])?;
        Ok(())
    }

    fn setup_table(connection: &rusqlite::Connection) -> Result<(), anyhow::Error> {
        connection.execute(
            "CREATE TABLE IF NOT EXISTS rustdoc_toolchain_crates_cache (
                name TEXT NOT NULL,
                cargo_fingerprint TEXT NOT NULL,
                root_item_id INTEGER NOT NULL,
                external_crates BLOB NOT NULL,
                paths BLOB NOT NULL,
                format_version INTEGER NOT NULL,
                items BLOB NOT NULL,
                item_id2delimiters BLOB NOT NULL,
                import_index BLOB NOT NULL,
                import_path2id BLOB NOT NULL,
                re_exports BLOB NOT NULL,
                PRIMARY KEY (name, cargo_fingerprint)
            )",
            [],
        )?;
        Ok(())
    }
}

#[derive(Debug, Clone)]
#[non_exhaustive]
struct ThirdPartyCrateCache {
    cache_workspace_packages: bool,
}

impl ThirdPartyCrateCache {
    fn new(
        connection: &rusqlite::Connection,
        cache_workspace_packages: bool,
    ) -> Result<Self, anyhow::Error> {
        Self::setup_table(connection)?;
        Ok(Self {
            cache_workspace_packages,
        })
    }

    /// Retrieve the cached documentation for a given package, if available.
    #[instrument(name = "Retrieve third-party crate docs from disk cache",
        skip_all,
        level=tracing::Level::DEBUG,
        fields(crate.id = %package_metadata.id(), cache_key = tracing::field::Empty, hit = tracing::field::Empty)
    )]
    fn get(
        &self,
        package_metadata: &PackageMetadata,
        cargo_fingerprint: &str,
        connection: &rusqlite::Connection,
        package_graph: &PackageGraph,
    ) -> Result<Option<RustdocCacheEntry>, anyhow::Error> {
        fn _get(
            package_metadata: &PackageMetadata,
            cargo_fingerprint: &str,
            connection: &rusqlite::Connection,
            cache_workspace_packages: bool,
            package_graph: &PackageGraph,
        ) -> Result<Option<RustdocCacheEntry>, anyhow::Error> {
            let Some(cache_key) = ThirdPartyCrateCacheKey::build(
                package_graph,
                package_metadata,
                cargo_fingerprint,
                cache_workspace_packages,
            ) else {
                return Ok(None);
            };
            tracing::Span::current().record("cache_key", tracing::field::debug(&cache_key));
            // Retrieve from rustdoc's output from cache, if available.
            let mut stmt = connection.prepare_cached(
                "SELECT
                        root_item_id,
                        external_crates,
                        paths,
                        format_version,
                        items,
                        item_id2delimiters,
                        import_index,
                        import_path2id,
                        re_exports,
                        annotated_items
                    FROM rustdoc_3d_party_crates_cache
                    WHERE crate_name = ? AND
                        crate_source = ? AND
                        crate_version = ? AND
                        crate_hash = ? AND
                        cargo_fingerprint = ? AND
                        rustdoc_options = ? AND
                        default_feature_is_enabled = ? AND
                        active_named_features = ?",
            )?;
            let span = tracing::trace_span!("Execute query");
            let guard = span.enter();
            let mut rows = stmt.query(params![
                cache_key.crate_name,
                cache_key.crate_source,
                cache_key.crate_version,
                // `NULL` values are considered to be distinct from all other values
                // by SQLite, including other `NULL`s. Therefore we use an empty
                // string as a placeholder for `NULL` values.
                cache_key.crate_hash.unwrap_or_default(),
                cache_key.cargo_fingerprint,
                cache_key.rustdoc_options,
                cache_key.default_feature_is_enabled,
                cache_key.active_named_features
            ])?;
            let Some(row) = rows.next().context("Failed to fetch next row")? else {
                return Ok(None);
            };
            drop(guard);

            let root_item_id = row.get_ref_unwrap(0).as_i64()?.try_into()?;
            let external_crates = row.get_ref_unwrap(1).as_bytes()?;
            let paths = row.get_ref_unwrap(2).as_bytes()?;
            let format_version = row.get_ref_unwrap(3).as_i64()?;

            let span = tracing::trace_span!("Copy items bytes buffer");
            let guard = span.enter();
            let items: Vec<u8> = row.get_unwrap(4);
            drop(guard);

            let item_id2delimiters = row.get_ref_unwrap(5).as_bytes()?;
            let import_index = row.get_ref_unwrap(6).as_bytes_or_null()?;
            let import_path2id = row.get_ref_unwrap(7).as_bytes_or_null()?;
            let re_exports = row.get_ref_unwrap(8).as_bytes_or_null()?;
            let annotated_items = row.get_ref_unwrap(9).as_bytes_or_null()?;

            let secondary_indexes =
                match (import_index, import_path2id, re_exports, annotated_items) {
                    (
                        Some(import_index),
                        Some(import_path2id),
                        Some(re_exports),
                        Some(annotated_items),
                    ) => Some(SecondaryIndexes {
                        import_index: Cow::Borrowed(import_index),
                        import_path2id: Cow::Borrowed(import_path2id),
                        re_exports: Cow::Borrowed(re_exports),
                        annotated_items: Some(Cow::Borrowed(annotated_items)),
                    }),
                    _ => None,
                };

            let krate = CachedData {
                root_item_id,
                external_crates: Cow::Borrowed(external_crates),
                paths: Cow::Borrowed(paths),
                format_version,
                items: Cow::Owned(items),
                item_id2delimiters: Cow::Borrowed(item_id2delimiters),
                secondary_indexes,
            }
            .hydrate(package_metadata.id().to_owned())
            .context("Failed to re-hydrate the stored docs")?;

            Ok(Some(krate))
        }
        let outcome = _get(
            package_metadata,
            cargo_fingerprint,
            connection,
            self.cache_workspace_packages,
            package_graph,
        );
        match &outcome {
            Ok(Some(_)) => {
                tracing::Span::current().record("hit", true);
            }
            Ok(None) => {
                tracing::Span::current().record("hit", false);
            }
            _ => {}
        }
        outcome
    }

    /// Store the JSON documentation generated by `rustdoc` in the cache.
    #[instrument(
        name = "Cache third-party crate docs to disk",
        skip_all,
        level=tracing::Level::DEBUG,
        fields(crate.id = %package_metadata.id(), cache_key = tracing::field::Empty))
    ]
    fn insert(
        &self,
        package_metadata: &PackageMetadata,
        krate: &crate::rustdoc::Crate,
        cargo_fingerprint: &str,
        connection: &rusqlite::Connection,
        cache_indexes: bool,
        package_graph: &PackageGraph,
    ) -> Result<(), anyhow::Error> {
        let Some(cache_key) = ThirdPartyCrateCacheKey::build(
            package_graph,
            package_metadata,
            cargo_fingerprint,
            self.cache_workspace_packages,
        ) else {
            return Ok(());
        };
        tracing::Span::current().record("cache_key", tracing::field::debug(&cache_key));
        let cached_data = if cache_indexes {
            CachedData::new(krate)
        } else {
            CachedData::raw(krate)
        }
        .context("Failed to serialize docs")?;
        let mut stmt = connection.prepare_cached(
            "INSERT INTO rustdoc_3d_party_crates_cache (
                crate_name,
                crate_source,
                crate_version,
                crate_hash,
                cargo_fingerprint,
                rustdoc_options,
                default_feature_is_enabled,
                active_named_features,
                root_item_id,
                external_crates,
                paths,
                format_version,
                items,
                item_id2delimiters,
                import_index,
                import_path2id,
                re_exports,
                annotated_items
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        )?;
        stmt.execute(params![
            cache_key.crate_name,
            cache_key.crate_source,
            cache_key.crate_version,
            // `NULL` values are considered to be distinct from all other values
            // by SQLite, including other `NULL`s. Therefore we use an empty
            // string as a placeholder for `NULL` values.
            cache_key.crate_hash.unwrap_or_default(),
            cache_key.cargo_fingerprint,
            cache_key.rustdoc_options,
            cache_key.default_feature_is_enabled,
            cache_key.active_named_features,
            cached_data.root_item_id,
            cached_data.external_crates,
            cached_data.paths,
            cached_data.format_version,
            cached_data.items,
            cached_data.item_id2delimiters,
            cached_data
                .secondary_indexes
                .as_ref()
                .map(|i| i.import_index.as_ref()),
            cached_data
                .secondary_indexes
                .as_ref()
                .map(|indexes| indexes.import_path2id.as_ref()),
            cached_data
                .secondary_indexes
                .as_ref()
                .map(|indexes| indexes.re_exports.as_ref()),
            cached_data
                .secondary_indexes
                .as_ref()
                .map(|indexes| indexes.annotated_items.as_ref())
        ])?;
        Ok(())
    }

    fn setup_table(connection: &rusqlite::Connection) -> Result<(), anyhow::Error> {
        connection.execute(
            "CREATE TABLE IF NOT EXISTS rustdoc_3d_party_crates_cache (
                crate_name TEXT NOT NULL,
                crate_source TEXT NOT NULL,
                crate_version TEXT NOT NULL,
                crate_hash TEXT NOT NULL,
                cargo_fingerprint TEXT NOT NULL,
                rustdoc_options TEXT NOT NULL,
                default_feature_is_enabled INTEGER NOT NULL,
                active_named_features TEXT NOT NULL,
                root_item_id INTEGER NOT NULL,
                external_crates BLOB NOT NULL,
                paths BLOB NOT NULL,
                format_version INTEGER NOT NULL,
                items BLOB NOT NULL,
                item_id2delimiters BLOB NOT NULL,
                annotated_items BLOB,
                import_index BLOB,
                import_path2id BLOB,
                re_exports BLOB,
                PRIMARY KEY (crate_name, crate_source, crate_version, crate_hash, cargo_fingerprint, rustdoc_options, default_feature_is_enabled, active_named_features)
            )",
            []
        )?;
        Ok(())
    }
}

#[derive(Debug)]
/// The serialized form of a crate's documentation, as stored in the cache.
pub(super) struct CachedData<'a> {
    root_item_id: u32,
    external_crates: Cow<'a, [u8]>,
    paths: Cow<'a, [u8]>,
    format_version: i64,
    items: Cow<'a, [u8]>,
    item_id2delimiters: Cow<'a, [u8]>,
    secondary_indexes: Option<SecondaryIndexes<'a>>,
}

#[derive(Debug)]
/// Data that can be computed starting from the raw JSON documentation for a crate,
/// without having to re-invoke `rustdoc`.
pub(super) struct SecondaryIndexes<'a> {
    import_index: Cow<'a, [u8]>,
    annotated_items: Option<Cow<'a, [u8]>>,
    import_path2id: Cow<'a, [u8]>,
    re_exports: Cow<'a, [u8]>,
}

impl<'a> CachedData<'a> {
    pub(super) fn new(krate: &'a crate::rustdoc::Crate) -> Result<CachedData<'a>, anyhow::Error> {
        let mut cached = Self::raw(krate)?;

        let import_index = bincode::serde::encode_to_vec(&krate.import_index, BINCODE_CONFIG)?;
        let annotated_items =
            bincode::serde::encode_to_vec(&krate.annotated_items, BINCODE_CONFIG)?;
        let import_path2id = bincode::serde::encode_to_vec(&krate.import_path2id, BINCODE_CONFIG)?;
        let re_exports = bincode::serde::encode_to_vec(&krate.external_re_exports, BINCODE_CONFIG)?;

        cached.secondary_indexes = Some(SecondaryIndexes {
            import_index: Cow::Owned(import_index),
            annotated_items: Some(Cow::Owned(annotated_items)),
            import_path2id: Cow::Owned(import_path2id),
            re_exports: Cow::Owned(re_exports),
        });
        Ok(cached)
    }

    /// Cache only `rustdoc`'s output, no secondary indexes.
    pub(super) fn raw(krate: &'a crate::rustdoc::Crate) -> Result<CachedData<'a>, anyhow::Error> {
        let crate_data = &krate.core.krate;
        let CrateItemIndex::Eager(index) = &crate_data.index else {
            anyhow::bail!(
                "The crate's item index is not deserialized. Are we trying to cache \
                the same crate twice? This is a bug."
            );
        };
        let mut items = Vec::new();
        let mut item_id2delimiters = HashMap::new();
        for (item_id, item) in &index.index {
            let start = items.len();
            serde_json::to_writer(&mut items, item)?;
            let end = items.len();
            item_id2delimiters.insert(item_id.0, (start, end));
        }
        let external_crates =
            bincode::serde::encode_to_vec(&crate_data.external_crates, BINCODE_CONFIG)?;
        let paths = bincode::serde::encode_to_vec(&crate_data.paths, BINCODE_CONFIG)?;

        Ok(CachedData {
            root_item_id: crate_data.root_item_id.0,
            external_crates: Cow::Owned(external_crates),
            paths: Cow::Owned(paths),
            format_version: crate_data.format_version as i64,
            items: Cow::Owned(items),
            item_id2delimiters: Cow::Owned(bincode::serde::encode_to_vec(
                &item_id2delimiters,
                BINCODE_CONFIG,
            )?),
            secondary_indexes: None,
        })
    }

    /// Re-hydrate the documentation retrieved from the cache.
    ///
    /// We hydrate all mappings eagerly, but we avoid re-hydrating the item index eagerly,
    /// since it can be quite large and deserialization can be slow for large crates.
    pub(super) fn hydrate(self, package_id: PackageId) -> Result<RustdocCacheEntry, anyhow::Error> {
        let span = tracing::trace_span!("Deserialize delimiters");
        let _guard = span.enter();
        let item_id2delimiters =
            bincode::serde::decode_from_slice(&self.item_id2delimiters, BINCODE_CONFIG)
                .context("Failed to deserialize item_id2delimiters")?
                .0;
        drop(_guard);

        let span = tracing::trace_span!("Deserialize paths");
        let _guard = span.enter();
        let paths = bincode::serde::decode_from_slice(&self.paths, BINCODE_CONFIG)
            .context("Failed to deserialize paths")?
            .0;
        drop(_guard);

        let crate_data = CrateData {
            root_item_id: rustdoc_types::Id(self.root_item_id.to_owned()),
            external_crates: bincode::serde::decode_from_slice(
                &self.external_crates,
                BINCODE_CONFIG,
            )
            .context("Failed to deserialize external_crates")?
            .0,
            paths,
            format_version: self.format_version.try_into()?,
            index: CrateItemIndex::Lazy(LazyCrateItemIndex {
                items: self.items.into_owned(),
                item_id2delimiters,
            }),
        };
        let Some(secondary_indexes) = self.secondary_indexes else {
            return Ok(RustdocCacheEntry::Raw(crate_data));
        };

        let core = crate::rustdoc::queries::CrateCore {
            package_id,
            krate: crate_data,
        };

        let span = tracing::trace_span!("Deserialize import_path2id");
        let _guard = span.enter();
        let import_path2id =
            bincode::serde::decode_from_slice(&secondary_indexes.import_path2id, BINCODE_CONFIG)
                .context("Failed to deserialize import_path2id")?
                .0;
        drop(_guard);

        let re_exports =
            bincode::serde::decode_from_slice(&secondary_indexes.re_exports, BINCODE_CONFIG)
                .context("Failed to deserialize re-exports")?
                .0;

        let import_index =
            bincode::serde::decode_from_slice(&secondary_indexes.import_index, BINCODE_CONFIG)
                .context("Failed to deserialize import_index")?
                .0;

        let annotated_items = if let Some(data) = secondary_indexes.annotated_items {
            bincode::serde::decode_from_slice(&data, BINCODE_CONFIG)
                .context("Failed to deserialize annotated_items")?
                .0
        } else {
            AnnotatedItems::default()
        };

        let krate = crate::rustdoc::Crate {
            core,
            annotated_items,
            import_path2id,
            external_re_exports: re_exports,
            import_index,
        };
        Ok(RustdocCacheEntry::Processed(krate))
    }
}

/// The key used to store and retrieve a crate's documentation from the cache.
///
/// It tries to capture all the information that can influence the output of the
/// relevant `rustdoc` command.
#[derive(Debug)]
pub(super) struct ThirdPartyCrateCacheKey<'a> {
    pub crate_name: &'a str,
    pub crate_source: Cow<'a, str>,
    pub crate_version: String,
    /// The hash of the crate's source code, computed via BLAKE3.
    /// It is only populated for path dependencies.
    pub crate_hash: Option<String>,
    pub cargo_fingerprint: &'a str,
    pub rustdoc_options: String,
    pub default_feature_is_enabled: bool,
    pub active_named_features: String,
}

impl<'a> ThirdPartyCrateCacheKey<'a> {
    /// Compute the cache key for a given package.
    pub(super) fn build(
        package_graph: &PackageGraph,
        package_metadata: &'a PackageMetadata<'a>,
        cargo_fingerprint: &'a str,
        cache_workspace_packages: bool,
    ) -> Option<ThirdPartyCrateCacheKey<'a>> {
        enum PathOrId<'a> {
            Path(Cow<'a, Utf8Path>),
            Id(&'a str),
        }

        impl<'a> From<PathOrId<'a>> for Cow<'a, str> {
            fn from(val: PathOrId<'a>) -> Self {
                match val {
                    PathOrId::Path(cow) => match cow {
                        Cow::Owned(path) => Cow::Owned(path.to_string()),
                        Cow::Borrowed(path) => Cow::Borrowed(path.as_str()),
                    },
                    PathOrId::Id(id) => Cow::Borrowed(id),
                }
            }
        }

        let source = match package_metadata.source() {
            guppy::graph::PackageSource::Workspace(p) => {
                if !cache_workspace_packages {
                    return None;
                }
                let p = package_graph.workspace().root().join(p);
                PathOrId::Path(Cow::Owned(p))
            }
            guppy::graph::PackageSource::Path(p) => PathOrId::Path(Cow::Borrowed(p)),
            guppy::graph::PackageSource::External(e) => PathOrId::Id(e),
        };
        let crate_hash = if let PathOrId::Path(package_path) = &source {
            let package_path = if package_path.is_relative() {
                package_graph.workspace().root().join(package_path)
            } else {
                package_path.clone().into_owned()
            };
            // We need to compute the hash of the package's contents,
            // to invalidate the cache when the package changes.
            // This is only relevant for path dependencies.
            // We don't need to do this for external dependencies,
            // since they are assumed to be immutable.
            let hash = match checksum_crate(&package_path) {
                Ok(hash) => hash,
                Err(e) => {
                    log_error!(
                        *e,
                        "Failed to compute the hash of the package at {}. \
                            I won't cache its JSON documentation to avoid serving stale data.",
                        package_metadata.id().repr()
                    );
                    return None;
                }
            };
            Some(hash.to_string())
        } else {
            None
        };
        let features = package_metadata
            .to_feature_set(StandardFeatures::Default)
            .features_for(package_metadata.id())
            .unwrap();
        let (default_feature_is_enabled, mut active_named_features) = match features {
            Some(f) => (f.has_base(), f.named_features().collect()),
            None => (false, vec![]),
        };
        active_named_features.sort();
        let cache_key = ThirdPartyCrateCacheKey {
            crate_name: package_metadata.name(),
            crate_source: source.into(),
            crate_version: package_metadata.version().to_string(),
            crate_hash,
            cargo_fingerprint,
            default_feature_is_enabled,
            // SQLite doesn't support arrays, so we have to serialize these two collections as strings.
            // This is well defined, since the order is well-defined.
            rustdoc_options: rustdoc_options().join(" "),
            active_named_features: active_named_features.join(" "),
        };
        Some(cache_key)
    }
}

/// Return the output of `cargo --verbose --version` for the nightly toolchain,
/// which can be used to fingerprint the toolchain used by Pavex.
pub fn cargo_fingerprint(toolchain_name: &str) -> Result<String, anyhow::Error> {
    let err_msg = || {
        format!(
            "Failed to run `cargo --verbose --version` on `{toolchain_name}`.\n\
Is the `{toolchain_name}` toolchain installed?\n\
If not, invoke\n

    rustup toolchain install {toolchain_name} -c rust-docs-json

to fix it.",
        )
    };
    let mut cmd = std::process::Command::new("rustup");
    cmd.arg("run")
        .arg(toolchain_name)
        .arg("cargo")
        .arg("--verbose")
        .arg("--version");
    let output = cmd.output().with_context(err_msg)?;
    if !output.status.success() {
        anyhow::bail!(err_msg());
    }
    let output = String::from_utf8(output.stdout).with_context(|| {
        format!("An invocation of `cargo --verbose --version` for the `{toolchain_name}` toolchain returned non-UTF8 data as output.")
    })?;
    Ok(output)
}
